/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.nexd.app.okhttp.internal.http;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import cn.nexd.app.okhttp.Challenge;
import cn.nexd.app.okhttp.Headers;
import cn.nexd.app.okhttp.Request;
import cn.nexd.app.okhttp.Response;
import cn.nexd.app.okhttp.internal.Platform;
import cn.nexd.app.okhttp.internal.Util;

/**
 * Headers and utilities for internal use by OkHttp.
 */
public final class OkHeaders {

    static final String PREFIX = Platform.get().getPrefix();

    /**
     * Synthetic response header: the local time when the request was sent.
     */
    public static final String SENT_MILLIS = PREFIX + "-Sent-Millis";

    /**
     * Synthetic response header: the local time when the response was received.
     */
    public static final String RECEIVED_MILLIS = PREFIX + "-Received-Millis";

    /**
     * Synthetic response header: the selected {@link cn.nexd.app.okhttp.Protocol protocol} ("spdy/3.1", "http/1.1",
     * etc).
     */
    public static final String SELECTED_PROTOCOL = PREFIX + "-Selected-Protocol";

    /**
     * Synthetic response header: the location from which the response was loaded.
     */
    public static final String RESPONSE_SOURCE = PREFIX + "-Response-Source";

    private OkHeaders() {
    }

    public static long contentLength(Request request) {
        return contentLength(request.headers());
    }

    public static long contentLength(Response response) {
        return contentLength(response.headers());
    }

    public static long contentLength(Headers headers) {
        return stringToLong(headers.get("Content-Length"));
    }

    private static long stringToLong(String s) {
        if (s == null) return -1;
        try {
            return Long.parseLong(s);
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    /**
     * Returns true if none of the Vary headers have changed between {@code cachedRequest} and {@code
     * newRequest}.
     */
    public static boolean varyMatches(
            Response cachedResponse, Headers cachedRequest, Request newRequest) {
        for (String field : varyFields(cachedResponse)) {
            if (!Util.equal(cachedRequest.values(field), newRequest.headers(field))) return false;
        }
        return true;
    }

    /**
     * Returns true if a Vary header contains an asterisk. Such responses cannot be cached.
     */
    public static boolean hasVaryAll(Response response) {
        return hasVaryAll(response.headers());
    }

    /**
     * Returns true if a Vary header contains an asterisk. Such responses cannot be cached.
     */
    public static boolean hasVaryAll(Headers responseHeaders) {
        return varyFields(responseHeaders).contains("*");
    }

    private static Set<String> varyFields(Response response) {
        return varyFields(response.headers());
    }

    /**
     * Returns the names of the request headers that need to be checked for equality when caching.
     */
    public static Set<String> varyFields(Headers responseHeaders) {
        Set<String> result = Collections.emptySet();
        for (int i = 0, size = responseHeaders.size(); i < size; i++) {
            if (!"Vary".equalsIgnoreCase(responseHeaders.name(i))) continue;

            String value = responseHeaders.value(i);
            if (result.isEmpty()) {
                result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
            }
            for (String varyField : value.split(",")) {
                result.add(varyField.trim());
            }
        }
        return result;
    }

    /**
     * Returns the subset of the headers in {@code response}'s request that impact the content of
     * response's body.
     */
    public static Headers varyHeaders(Response response) {
        // Use the request headers sent over the network, since that's what the
        // response varies on. Otherwise OkHttp-supplied headers like
        // "Accept-Encoding: gzip" may be lost.
        Headers requestHeaders = response.networkResponse().request().headers();
        Headers responseHeaders = response.headers();
        return varyHeaders(requestHeaders, responseHeaders);
    }

    /**
     * Returns the subset of the headers in {@code requestHeaders} that impact the content of
     * response's body.
     */
    public static Headers varyHeaders(Headers requestHeaders, Headers responseHeaders) {
        Set<String> varyFields = varyFields(responseHeaders);
        if (varyFields.isEmpty()) return new Headers.Builder().build();

        Headers.Builder result = new Headers.Builder();
        for (int i = 0, size = requestHeaders.size(); i < size; i++) {
            String fieldName = requestHeaders.name(i);
            if (varyFields.contains(fieldName)) {
                result.add(fieldName, requestHeaders.value(i));
            }
        }
        return result.build();
    }

    /**
     * Returns true if {@code fieldName} is an end-to-end HTTP header, as defined by RFC 2616,
     * 13.5.1.
     */
    static boolean isEndToEnd(String fieldName) {
        return !"Connection".equalsIgnoreCase(fieldName)
                && !"Keep-Alive".equalsIgnoreCase(fieldName)
                && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
                && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
                && !"TE".equalsIgnoreCase(fieldName)
                && !"Trailers".equalsIgnoreCase(fieldName)
                && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
                && !"Upgrade".equalsIgnoreCase(fieldName);
    }

    /**
     * Parse RFC 2617 challenges. This API is only interested in the scheme name and realm.
     */
    public static List<Challenge> parseChallenges(Headers responseHeaders, String challengeHeader) {
        // auth-scheme = token
        // auth-param  = token "=" ( token | quoted-string )
        // challenge   = auth-scheme 1*SP 1#auth-param
        // realm       = "realm" "=" realm-value
        // realm-value = quoted-string
        List<Challenge> result = new ArrayList<Challenge>();
        for (int i = 0, size = responseHeaders.size(); i < size; i++) {
            if (!challengeHeader.equalsIgnoreCase(responseHeaders.name(i))) {
                continue;
            }
            String value = responseHeaders.value(i);
            int pos = 0;
            while (pos < value.length()) {
                int tokenStart = pos;
                pos = HeaderParser.skipUntil(value, pos, " ");

                String scheme = value.substring(tokenStart, pos).trim();
                pos = HeaderParser.skipWhitespace(value, pos);

                // TODO: This currently only handles schemes with a 'realm' parameter;
                //       It needs to be fixed to handle any scheme and any parameters
                //       http://code.google.com/p/android/issues/detail?id=11140

                if (!value.regionMatches(true, pos, "realm=\"", 0, "realm=\"".length())) {
                    break; // Unexpected challenge parameter; give up!
                }

                pos += "realm=\"".length();
                int realmStart = pos;
                pos = HeaderParser.skipUntil(value, pos, "\"");
                String realm = value.substring(realmStart, pos);
                pos++; // Consume '"' close quote.
                pos = HeaderParser.skipUntil(value, pos, ",");
                pos++; // Consume ',' comma.
                pos = HeaderParser.skipWhitespace(value, pos);
                result.add(new Challenge(scheme, realm));
            }
        }
        return result;
    }
}
